(define-module (grump lines)
  #:use-module (grump control)
  #:use-module (ice-9 match)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 textual-ports)
  #:use-module (srfi srfi-41)
  #:use-module (srfi srfi-43)
  #:export (fold-lines
            read-lines
            read-lines->vector
            read-lines->stream
            next-line
            find-line
            filter-lines
            take-lines-before
            take-lines-after
            take-lines-between))

(define (delim-handler handle-delim)
  (case handle-delim
    ((split)  values)
    ((trim)   car)
    ((concat) (match-lambda
                ((line . delim)
                 (if (and (string? line) (char? delim))
                     (string-append line (string delim))
                     line))))))

(define* (fold-lines proc init #:optional
                     (port (current-input-port))
                     (handle-delim 'trim))
  "The fundamental line-based port iterator.  Calls (PROC LINE RESULT) on
each newline-delimited LINE read from PORT until end-of-file is reached.
RESULT is the return value from the previous call to PROC, or INIT on
the first call.  Returns the result of the final call to PROC, or INIT
if no lines were read.  If PORT is not given, it defaults to the current
input port.

If HANDLE-DELIM is specified, it should be one of the following symbols:

  `trim'    Discard the terminating delimiter (default)
  `concat'  Append the terminating delimiter to LINE
  `split'   Pass a pair of (LINE . DELIM) as LINE
"
  (define unpack
    (delim-handler handle-delim))
  (let loop ((result init))
    (match (%read-line port)
      (((? eof-object?) . _)
       result)
      (line/delim
       (loop (proc (unpack line/delim) result))))))

(define* (read-lines #:optional
                     (port (current-input-port))
                     (handle-delim 'trim))
  "Return a list of all lines read from PORT, or an empty list if no lines
were read.  PORT defaults to the current input port.  See `fold-lines'
for a description of the HANDLE-DELIM argument."
  (reverse (fold-lines cons '() port handle-delim)))

(define* (read-lines->vector #:optional
                             (port (current-input-port))
                             (handle-delim 'trim))
  "Return a vector containing all lines read from PORT, or an empty vector
if no lines were read.  PORT defaults to the current input port.  See
`fold-lines' for a description of the HANDLE-DELIM argument."
  (reverse-list->vector (fold-lines cons '() port handle-delim)))

(define* (read-lines->stream #:optional
                             (port (current-input-port))
                             (handle-delim 'trim))
  "Return a SRFI-41 stream which will lazily read lines from PORT.  PORT
defaults to the current input port.  See `fold-lines' for a description
of the HANDLE-DELIM argument."
  (define unpack
    (delim-handler handle-delim))
  (stream-let loop ()
    (match (%read-line port)
      (((? eof-object?) . _)
       stream-null)
      (line/delim
       (stream-cons (unpack line/delim) (loop))))))

(define* (next-line #:optional
                    (port (current-input-port))
                    (handle-delim 'trim))
  "Return the next line read from PORT, or `#f' at end-of-file.  PORT
defaults to the current input port.  See `fold-lines for a description
of the HANDLE-DELIM argument.'"
  (define unpack
    (delim-handler handle-delim))
  (match (%read-line port)
    (((? eof-object?) . _)
     #f)
    (line/delim
     (unpack line/delim))))

(define* (find-line #:optional
                    (pred (const #t))
                    (port (current-input-port))
                    (handle-delim 'trim))
  "Return the first line read from PORT for which (PRED LINE) returns
true, or `#f' if no line matched.  PORT defaults to the current input
port.  See `fold-lines' for a description of the HANDLE-DELIM argument."
  (call-with-return
   (lambda (return)
     (fold-lines (lambda (line result)
                   (when (pred line)
                     (return line))
                   result)
                 #f port handle-delim))))

(define* (filter-lines pred #:optional
                       (port (current-input-port))
                       (handle-delim 'trim))
  "Return a list of all lines read from PORT for which (PRED LINE) returns
true, or an empty list if no lines matched.  PORT defaults to the
current input port.  See `fold-lines' for a description of the
HANDLE-DELIM argument."
  (reverse (fold-lines (lambda (line result)
                         (if (pred line)
                             (cons line result)
                             result))
                       '() port handle-delim)))

(define* (take-lines-before pred #:optional
                            (port (current-input-port))
                            (handle-delim 'trim))
  "Return a list of lines read from PORT before the first line for
which (PRED LINE) returns true.  The line which matches will be left in
the port buffer for subsequent reads.  PORT defaults to the current
input port.  See `fold-lines' for a description of the HANDLE-DELIM
argument."
  (define unpack
    (delim-handler handle-delim))
  (reverse (call-with-return
            (lambda (return)
              (fold-lines
               (lambda (line/delim result)
                 (let ((line (unpack line/delim)))
                   (when (pred line)
                     (when (char? (cdr line/delim))
                       (unget-char port (cdr line/delim)))
                     (unget-string port (car line/delim))
                     (return result))
                   (cons line result)))
               '() port 'split)))))

(define* (take-lines-after pred #:optional
                           (port (current-input-port))
                           (handle-delim 'trim))
  "Return a list of lines read from PORT after the first line for
which (PRED LINE) returns true.  The line which matches will not be
included in the result.  PORT defaults to the current input port.  See
`fold-lines' for a description of the HANDLE-DELIM argument."
  (if (find-line pred port handle-delim)
      (read-lines port handle-delim)
      '()))

(define* (take-lines-between pred1 pred2 #:optional
                             (port (current-input-port))
                             (handle-delim 'trim))
  "Return a list of lines read from PORT after the first line
matching (PRED1 LINE) and before the following line matching (PRED2
LINE).  The line matching PRED1 will not be included in the result.  The
line matching PRED2 will be left in the port buffer for subsequent
reads.  PORT defaults to the current input port.  See `fold-lines' for a
description of the HANDLE-DELIM argument."
  (if (find-line pred1 port handle-delim)
      (take-lines-before pred2 port handle-delim)
      '()))
